```latex
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Copyright by Wenliang Du.                                        %%
%% This work is licensed under the Creative Commons                 %%
%% Attribution-NonCommercial-ShareAlike 4.0 International License. %%
%% To view a copy of this license, visit                            %%
%% http://creativecommons.org/licenses/by-nc-sa/4.0/.               %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcommand{\commonfolder}{../../common-files}

\input{\commonfolder/header}
\input{\commonfolder/copyright}


\lhead{\bfseries SEED Labs -- Return-to-libc Attack Lab}

\newcommand{\retFigs}{./Figs}

\def \code#1 {\fbox{\scriptsize{\texttt{#1}}}}

\begin{document}

\begin{center}
{\LARGE 返还到libc攻击实验}
\end{center}

\seedlabcopyright{2006 - 2020}


% *******************************************
% SECTION
% ******************************************* 
\section{概述}

本实验的学习目标是让学生获得对一种有趣的缓冲区溢出攻击的直接体验；这种攻击可以绕过当前在主要Linux操作系统中实现的安全机制。利用缓冲区溢出漏洞的一种常见方法是用恶意Shellcode填充缓冲区，然后使易受攻击的程序跳转到存储在堆栈中的Shellcode。为了防止这类攻击，一些操作系统允许程序使其堆栈不可执行；因此，跳转到Shellcode会使程序失败。

不幸的是，上述保护机制并不是万无一失的。存在一种称为“返回到libc”的缓冲区溢出攻击变种，它不需要可执行堆栈；甚至不使用Shellcode。相反，它会促使易受攻击的程序跳转到一些现有代码，例如`libc`库中的`system()`函数，该函数已经加载在进程的内存空间中。

在这个实验中，给定一个存在缓冲区溢出漏洞的程序；任务是开发一个“返回到libc”攻击来利用此漏洞并最终获取root权限。除了攻击外，学生还将被指导穿越Ubuntu中实现的一些保护机制以抵消缓冲区溢出攻击。本实验涵盖了以下主题：

\begin{itemize}[noitemsep]
    \item 缓冲区溢出漏洞
    \item 函数调用中的堆栈布局及不可执行堆栈 
    \item “返回到libc”攻击和返回导向编程（ROP）
\end{itemize}

\paragraph{参考文献与视频。}
有关“返回到libc”攻击的详细内容可以在以下位置找到：

\begin{itemize}
    \item 《SEED书籍》中的第5章，\seedbook
    \item 在Udemy上的《SEED讲座》，\seedcsvideo
\end{itemize}

\paragraph{实验环境。} \seedenvironmentC

\paragraph{教师提示。}
教师可以通过选择易受攻击程序中缓冲区的大小来定制此实验。
详情请参阅第~\ref{sec:vulnerable_program}节。

% *******************************************
% SECTION
% ******************************************* 
\section{环境设置}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{x86和x64架构注意事项}

在x64机器（64位）上实现“返回到libc”攻击比在x86机器（32位）上更难。尽管SEED Ubuntu 20.04虚拟机是一台64位机器，但我们决定继续使用32位程序。未来我们可能会为这个实验引入一个64位版本。
因此，在本实验中，当使用`gcc`编译程序时，始终使用`-m32`标志，这意味着将程序编译成32位二进制文件。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{禁用防护措施}

可以使用我们的预构建Ubuntu虚拟机执行实验任务。Ubuntu和其他Linux发行版已实现多种安全机制，使缓冲区溢出攻击变得困难。
为了简化我们的攻击，我们需要首先禁用这些防护措施。

\paragraph{地址空间随机化。}
Ubuntu和一些其他基于Linux的系统使用地址空间随机化来随机化堆栈和堆的起始地址，这使得猜测确切的地址变得更加困难。猜测地址是缓冲区溢出攻击中的关键步骤之一。在本实验中，我们通过以下命令禁用此功能：

\begin{lstlisting}
$ sudo sysctl -w kernel.randomize_va_space=0
\end{lstlisting}

\paragraph{StackGuard保护方案。}
`gcc`编译器实现了一个称为“StackGuard”的安全机制，以防止缓冲区溢出。在存在这种防护的情况下，缓冲区溢出攻击将不会起作用。我们可以在编译时通过使用`-fno-stack-protector`选项来禁用此防护。
例如，要编译一个名为`example.c`的程序且不启用StackGuard保护，可以执行以下操作：

\begin{lstlisting}
$ gcc -m32 -fno-stack-protector example.c
\end{lstlisting}

\paragraph{不可执行堆栈。} Ubuntu以前允许堆栈可执行，
但现在已改变。
程序（以及共享库）必须声明它们是否需要可执行的堆栈或不需要，也就是说，它们需要在程序头中标记一个字段。内核或动态链接器会根据此标记决定该运行中的程序的堆栈是可执行还是不可执行。这种标记由较新的版本的`gcc`自动完成，并且默认情况下堆栈设置为不可执行。
要更改此设置，请在编译程序时使用以下选项：

\begin{lstlisting}
对于可执行堆栈:
$ gcc -m32 -z execstack  -o test test.c

对于不可执行堆栈:
$ gcc -m32 -z noexecstack  -o test test.c
\end{lstlisting}

由于本实验的目标是展示不可执行堆栈保护不起作用，你应该在本实验中始终使用`-z noexecstack`选项来编译你的程序。

\paragraph{配置`\bin/sh`。} 在Ubuntu 20.04中，
`\bin/sh`符号链接指向
`\bin/dash` shell。
`\texttt{dash}`程序在这两个虚拟机之间有一个重要差异。
`\texttt{dash}` shell具有一个预防措施，防止其在\setuid进程下执行。
如果`\texttt{dash}`在\setuid进程中被执行，
它会立即更改有效用户ID到进程的实际用户ID，基本上放弃了它的特权。
Ubuntu 12.04中的`\texttt{dash}`程序没有这种行为。

由于我们的受害者程序是一个\setuid程序，并且我们的攻击使用`system()`函数运行我们选择的命令。此函数不会直接运行我们的命令；它会调用`\bin/sh`来执行我们的命令。因此，`\bin/dash`中的预防措施在执行我们的命令之前立即降低了\setuid特权，使我们的攻击更困难。
为了禁用这种保护，
我们将`\bin/sh`链接到另一个没有此类预防措施的shell。
我们已经在Ubuntu 16.04虚拟机中安装了一个称为`\texttt{zsh}`的shell程序。使用以下命令将`\bin/sh`链接到`\texttt{zsh}`：

\begin{lstlisting}
$ sudo ln -sf /bin/zsh /bin/sh
\end{lstlisting}

需要注意的是，实现于`\texttt{dash}`中的预防措施可以被绕过。
我们将在后续任务中做到这一点。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{易受攻击的程序}
\label{sec:vulnerable_program}

\begin{lstlisting}[caption={易受攻击的程序（`retlib.c`）}]
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#ifndef BUF_SIZE
#define BUF_SIZE 12
#endif

int bof(char *str)
{
    char buffer[BUF_SIZE];
    unsigned int *framep;

    // 将ebp复制到framep
    asm("movl %%ebp, %0" : "=r" (framep));      

    /* 输出实验所需的有关信息 */
    printf("buffer[] 在 bof() 中的地址:  0x%.8x\n", (unsigned)buffer);
    printf("bof() 中的帧指针值:  0x%.8x\n", (unsigned)framep);

    strcpy(buffer, str);   (*@\reflectbox{\ding{222}} \textbf{缓冲区溢出！} @*)

    return 1;
}

int main(int argc, char **argv)
{
   char input[1000];
   FILE *badfile;

   badfile = fopen("badfile", "r");
   int length = fread(input, sizeof(char), 1000, badfile);
   printf("input[] 在 main() 中的地址:  0x%x\n", (unsigned int) input);
   printf("输入大小: %d\n", length);

   bof(input);

   printf("(^_^)(^_^) 能够正常返回 (^_^)(^_^)\n");
   return 1;
}

// 这个函数将在可选任务中使用
void foo(){
    static int i = 1;
    printf("foo() 函数被调用 %d 次\n", i++);
    return;
}
\end{lstlisting}

上述程序具有缓冲区溢出漏洞。它首先从名为`badfile`的文件读取最多1000字节的数据。
然后，将输入数据传递给`bof()`函数，该函数使用`strcpy()`将其复制到其内部缓冲区中。但是，内部缓冲区大小小于1000，
因此存在潜在的缓冲区溢出漏洞。

这是一个拥有root权限的\setuid程序，因此如果普通用户能够利用此缓冲区溢出漏洞，他们可能会获取root shell。
需要注意的是，该程序从名为`badfile`的文件中获取其输入，而该文件由用户提供。因此，
我们可以构造一个文件的方式是当易受攻击的程序将文件内容复制到其缓冲区时，
可以生成一个root shell。

\vspace{0.1in}
\paragraph{编译。} 
首先编译代码并将其转换为一个root拥有权限的\setuid程序。
别忘了包括
`-fno-stack-protector`选项（用于关闭StackGuard保护）和
`" -z noexecstack"`选项（用于开启不可执行堆栈保护）。 
还应注意到，在更改所有权之前必须先打开\setuid位， 
因为所有权更改会导致\setuid位被关闭。
所有这些命令都在提供的`Makefile`中包括。

\begin{lstlisting}
// 注意：N 应替换为教师设置的值
$ gcc -m32 -DBUF_SIZE=N -fno-stack-protector -z noexecstack -o retlib retlib.c
$ sudo chown root retlib           
$ sudo chmod 4755 retlib           
\end{lstlisting}

\paragraph{给教师。} 
为了避免学生使用过去（或在网上公布）的解决方案，
讲师可以更改`BUF_SIZE`的值，要求学生使用不同的`BUF_SIZE`值编译代码。
如果没有`-DBUF_SIZE`
选项，则`BUF_SIZE`设置为默认值12（在程序中定义）。
当这个值发生变化时，堆栈布局也会变化，并且解决方案也将不同。 
学生应该向他们的讲师询问
`N`的值。 `N`可以在提供的`Makefile`中设置，
并可以是 10 到 800 之间的任意值。

% *******************************************
% SECTION
% ******************************************* 
\section{实验任务}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务1：查找libc函数的地址} 

在 \linux 中，当一个程序运行时，`libc`库将被加载到内存中。当启用地址空间随机化时，
对于相同的程序，库总是加载在同一内存地址（不同的程序可能具有不同的`libc`库的内存地址）。
因此，我们可以使用`gdb`等调试工具轻松找到`system()`函数的地址。具体来说，我们可以在目标程序`retlib`中进行调试。
尽管该程序是一个root拥有权限的\setuid程序，
我们仍然可以对其进行调试（即，有效用户ID将与实际用户ID相同）。
在`gdb`内需要键入`run`命令来执行一次目标程序；
否则，库代码将不会加载。
我们将使用`p`命令（或`print`）打印出
`system()`和`exit()`函数的地址（稍后我们需要`exit()`）。

\begin{lstlisting}
$ touch badfile
$ gdb -q retlib     (*@\reflectbox{\ding{217}} 使用“静默”模式@*)
Reading symbols from ./retlib...
(No debugging symbols found in ./retlib)
gdb-peda$ break main
Breakpoint 1 at 0x1327
gdb-peda$ run
......
Breakpoint 1, 0x56556327 in main ()
gdb-peda$ p system
$1 = {<text variable, no debug info>} (*@\textbf{0xf7e12420}@*) <system>
gdb-peda$ p exit
$2 = {<text variable, no debug info>} (*@\textbf{0xf7e04f80}@*) <exit>
gdb-peda$ quit
\end{lstlisting}

需要注意的是，即使对于同一个程序，
如果将其从 \setuid 程序更改为非\setuid 程序，
`libc`库可能不会加载到相同的位置。因此，
当我们调试程序时需要调试目标\setuid程序；
否则，我们获得的地址可能是错误的。

\paragraph{运行gdb批处理模式。} 如果你更喜欢以批处理模式运行gdb
你可以将gdb命令放在一个文件中，并然后请求gdb从这个文件执行这些命令：

\begin{lstlisting}
$ cat gdb_command.txt
break main
run
p system
p exit
quit
$ gdb -q -batch -x gdb_command.txt ./retlib
...
Breakpoint 1, 0x56556327 in main ()
$1 = {<text variable, no debug info>} 0xf7e12420 <system>
$2 = {<text variable, no debug info>} 0xf7e04f80 <exit>
\end{lstlisting}
 

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务2：将Shell字符串放入内存}

我们的攻击策略是跳转到`system()`函数，并使其执行任意命令。因为我们希望获得一个shell提示符，所以需要让`system()`函数执行`"/bin/sh"`程序。
因此，`"/bin/sh"`的命令字符串必须先放在内存中并知道其地址（此地址需要传递给`system()`函数）。有许多方法可以实现这些目标；我们选择一种使用环境变量的方法。鼓励学生采用其他方法。

当我们从shell提示符运行一个程序时，
实际上是启动了一个子进程来执行该程序，并将所有导出的shell变量作为子进程的环境变量。
这为我们提供了一种简单的方式来在子进程中放入一些任意字符串。
让我们定义一个新的shell变量`MYSHELL`，并让它包含字符串`"/bin/sh"`。从以下命令中我们可以验证这个字符串进入了子进程，
并且由子进程中的`env`命令打印出来。

\begin{lstlisting}
$ export MYSHELL=/bin/sh
$ env | grep MYSHELL
MYSHELL=/bin/sh
\end{lstlisting}

我们将使用此变量的地址作为`system()`调用的一个参数。内存中该变量的位置可以通过以下程序轻松找到：

\begin{lstlisting}
void main(){
   char* shell =  getenv("MYSHELL");
   if (shell) 
      printf("%x\n", (unsigned int)shell);
}
\end{lstlisting}

将上述代码编译成名为`prtenv`的二进制文件。
如果禁用地址空间随机化，你会发现相同的地址被打印出来。
当你在同一个终端运行易受攻击程序`retlib`
时，环境变量的地址将是相同的（见特殊注释）。 
你可以通过将上述代码放入 `retlib.c` 中来验证这一点。但是
程序名长度确实会有所不同。这就是我们选择6个字符作为程序名称`prtenv`的原因，以匹配`retlib`的长度。

\paragraph{注释。} 编译上述程序时应使用`-m32`标志，
所以二进制代码`prtenv`
将针对32位机器而不是针对64位机器。
易受攻击程序`retlib`是一个32位二进制文件，因此如果`prtenv`是64位的，则环境变量的地址将会不同。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务3：发起攻击}

我们已经准备好创建 `badfile` 的内容。因为其内容涉及一些二进制数据（例如，`libc` 函数的地址），我们可以使用 Python 来进行构建。
我们在下面提供了一个代码框架，核心部分留待你填完。

\begin{lstlisting}
#!/usr/bin/env python3
import sys

# 填充内容为非零值
content = bytearray(0xaa for i in range(300))

X = 0
sh_addr = 0x00000000       # "/bin/sh" 的地址
content[X:X+4] = (sh_addr).to_bytes(4,byteorder='little')

Y = 0
system_addr = 0x00000000   # `system()` 函数的地址
content[Y:Y+4] = (system_addr).to_bytes(4,byteorder='little')

Z = 0
exit_addr = 0x00000000     # `exit()` 函数的地址
content[Z:Z+4] = (exit_addr).to_bytes(4,byteorder='little')

# 将内容保存到文件中
with open("badfile", "wb") as f:
  f.write(content)
\end{lstlisting}
 
你需要确定三个地址以及 \texttt{X}，\texttt{Y} 和 \texttt{Z} 的值。
如果这些值不正确，
你的攻击可能不会成功。在报告中，你必须
描述如何决定 \{\tt X}，\{\tt Y} 和 {\tt Z} 的值。
或者如果你使用了试错法，请展示你的尝试。

\paragraph{关于gdb的注释。} 如果您使用gdb来确定 
`X`，`Y` 和 `Z` 的值，
需要注意的是
在Ubuntu 20.04中，gdb的行为略有不同 
与Ubuntu 16.04中。特别是，在我们在函数bof设置了一个断点后，
当gdb停在bof()函数内部时，
它会停在ebp寄存器被设置到当前堆栈帧之前的地方，
因此如果在这里打印出ebp的值，我们将会得到调用者的ebp值，而不是bof()'s ebp。我们需要输入`next`
来执行几条指令并在修改了指向bof()函数堆栈帧的ebp寄存器后停在。
SEED书籍（第二版）基于Ubuntu 16.04，
因此没有这个`next`步骤。

\paragraph{攻击变体1：}
`exit()` 函数真的有必要吗？请尝试
不将此函数的地址包含在 `badfile` 中的攻击。再次运行攻击，报告并解释你的观察结果。  



\paragraph{攻击变体2:} 
成功完成攻击后，
更改`retlib`文件名为不同的名称，确保新文件名长度不同。
例如，你可以将其更改为`newretlib`。重复不改变`badfile`内容的攻击。
攻击是否会成功？ 如果没有成功，请解释原因。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务4：击败Shell的预防措施}

本任务的目标是在启用了Shell的预防措施后执行返回到libc的攻击。在进行任务1和2之前，我们重新链接`/bin/sh`指向`/bin/zsh`，
而不是指向`/bin/dash`(原设置)。这是因为某些shell程序（如dash和bash）具有一个自动
当它们在\setuid进程中运行时会丢失特权的预防措施。
在这个任务中，我们希望击败这样的预防措施，即即使/bin/sh仍然指向/bin/dash，
我们也能够获得root shell权限。

让我们首先将符号链接恢复为原来的设置：

\begin{lstlisting}
$ sudo ln -sf /bin/dash /bin/sh
\end{lstlisting}

尽管`dash`和`bash`都会丢失\setuid特权，但它们在使用`-p`选项时不会这样做。
当我们返回到`system`函数时，
此函数调用`/bin/sh`，但它不使用`-p`选项。因此，目标程序的\setuid
特权将被撤销。如果有一个可以让我们直接执行`"/bin/bash -p"`而不经过`system`函数的功能，我们仍然可以获得root权限。

实际上有许多libc函数都能做到这一点，例如`exec()`家族的一些函数，
包括`execl()`, `execle()`, `execv()`等。
让我们看看`execv()`函数：

\begin{lstlisting}
int execv(const char *pathname, char *const argv[]);
\end{lstlisting}

此函数接受两个参数：一个是命令地址，另一个是用于命令的参数数组地址。例如，如果我们想使用`execv`调用`"/bin/bash -p"`，
我们需要设置以下内容：

\begin{lstlisting}
pathname = "/bin/bash" 的地址
argv[0]  = "/bin/bash" 的地址
argv[1]  = "-p" 的地址
argv[2]  = NULL (即，4个字节的零).
\end{lstlisting}

从前面的任务中我们能够轻松地获得两个涉及字符串的地址。因此，
如果我们能在堆上构造`argv[]`数组并获取其地址，
我们将拥有进行返回到libc攻击所需的一切。
这次我们需要返回到`execv()`函数。

这里有一个需要注意的地方：`argv[2]`必须为零（一个整数0，四个字节）。如果我们在输入中放入四个零，`strcpy()`会在第一个零处终止；
无论其后面的内容都不会被复制到`bof()`函数的缓冲区。这似乎是一个问题，
但请记住，在你的输入中的所有内容都已经在堆上；它们在`main()`函数的缓冲区中。
要获得此缓冲区的地址并不难。为了简化任务，我们已经让易受攻击程序为您打印出该地址。

与第3个任务相似，你需要构建你所需的输入，
所以当`bof()`函数返回时，它会返回到`execv()`
该函数从堆中获取`"/bin/bash"`字符串和`argv[]`数组的地址。
你需要在堆上准备好一切，
以便`execv()`执行后
可以执行`"/bin/bash -p"`并为您提供root shell。在你的报告中，请描述你如何构造输入。

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{任务5（可选）：返回导向编程}

解决第4个任务的问题有许多方法。
另一种方法是在调用`system()`之前先调用`setuid(0)`。`setuid(0)`函数将真实用户ID和有效用户ID都设置为0，使进程变成一个非\setuid的程序（它仍然拥有root权限）。
这种方法需要我们串联两个函数一起使用。这种方法进一步推广到串联多个函数，并且进一步推广到串联多段代码。
这导致了返回导向编程 (ROP)。

使用 ROP 来解决第4个任务的问题相当复杂，
并且超出了这个实验的范围。然而，我们确实希望给学生一个关于 ROP 的味道，要求他们
在 ROP 特殊情况上工作。
`retlib.c` 程序中有一个名为 `foo()` 的函数，但在程序中从未被调用。该功能是为此任务设计的。你的任务是在程序中利用缓冲区溢出问题，
所以当程序从 `bof()` 函数返回时，它会连续10次调用 `foo()`
在给你 root shell 之前。
在你的实验报告中你需要描述你输入是如何构建的。

这里是你最终的结果看起来的样子：

\begin{lstlisting}
$ ./retlib
...
Function foo() is invoked 1 times
Function foo() is invoked 2 times
Function foo() is invoked 3 times
Function foo() is invoked 4 times
Function foo() is invoked 5 times
Function foo() is invoked 6 times
Function foo() is invoked 7 times
Function foo() is invoked 8 times
Function foo() is invoked 9 times
Function foo() is invoked 10 times
bash-5.0#   (*@\reflectbox{\ding{222}} 获得 root shell！@*)
\end{lstlisting}

\paragraph{指南。} 让我们回顾一下我们在任务3中做了什么。
我们构建了堆栈上的数据，使得当程序从`bof()`返回时，
它会跳转到`system()`函数，在`system()`返回后
程序将跳转到`exit()`函数。
我们将采用类似的策略在这里工作。 
而不是跳转到`system()`和`exit()`，我们会构建堆栈上的数据，
这样当程序从`bof`返回时，它会返回到`foo`；
当`foo`返回时，它会返回到另一个`foo`。
重复此操作10次后，在第十个`foo`返回时，它将返回
跳转至 `execv()` 函数来给我们 root shell。  

\paragraph{进一步阅读。} 我们在这个任务中所做的只是 ROP 的一个特殊情况。
你可能会注意到`foo()`函数没有接受任何参数。如果它有，
连续调用10次将会变得更为复杂。通用的 ROP 技术允许你按顺序调用任意数量的函数，每个函数可以有多个参数。
SEED 书籍（第二版）详细介绍了如何使用这种通用的 ROP 技巧来解决第4个任务中的问题，它涉及四次`sprintf()`调用，
然后是 `setuid(0)` 的一次调用，在执行前再调用一次 `system("/bin/sh")` 来给我们 root shell。方法相当复杂，需要15页才能在 SEED 书中解释清楚。

% *******************************************
% SECTION
% ******************************************* 
\section{指南：理解函数调用机制}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{了解堆栈布局}

为了知道如何进行“返回到libc”攻击，我们需要
理解堆栈是如何工作的。我们使用一个小型的C程序来了解函数调用对堆栈的影响。
更详细的解释可以在SEED书籍和SEED讲座中找到。

```foobarc.c
#include<stdio.h>
void foo(int x)
{
  printf("Hello world: %d\n", x);
}

int main()
{
  foo(1);
  return 0;
}
```

我们可以使用`gcc -m32 -S foobar.c`编译这个程序到汇编代码。
生成的文件foobar.s的内容如下：

```assembly
    ......
  8 foo:
  9         pushl   %ebp
 10         movl    %esp, %ebp
 11         subl    $8, %esp
 12         movl    8(%ebp), %eax   
 13         movl    %eax, 4(%esp)
 14         movl    $.LC0, (%esp)  : string "Hello world: %d\n"
 15         call    printf
 16         leave
 17         ret
    ......
 21 main:
 22         leal    4(%esp), %ecx
 23         andl    $-16, %esp
 24         pushl   -4(%ecx)
 25         pushl   %ebp
 26         movl    %esp, %ebp
 27         pushl   %ecx
 28         subl    $4, %esp
 29         movl    $1, (%esp)
 30         call    foo
 31         movl    $0, %eax
 32         addl    $4, %esp
 33         popl    %ecx
 34         popl    %ebp
 35         leal    -4(%ecx), %esp
 36         ret
```

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{调用和进入`foo()`函数}

让我们集中在调用`foo()`时的堆栈上。在这一解释中可以忽略之前的堆栈。

\begin{figure}[htb]
	\centering
	\includegraphics[width=0.95\textwidth]{\retFigs/enter_leave_foo.pdf}
	\caption{进入和离开`foo()`函数}
	\label{fig:enter_leave_foo}
\end{figure}

\begin{itemize}
    \item \textbf{第28-29行：} 这两行语句将值1（即传给foo()的参数）压入堆栈。这会使得esp指针增加4个字节。
      压入堆栈后的堆栈状态如图\ref{fig:enter_leave_foo}(a)所示。

    \item \textbf{第30行：`call foo`}：这条语句将当前指令的地址（即返回地址）推入堆栈，
     然后跳转到foo()函数中的代码。
      压入堆栈后的状态如图\ref{fig:enter_leave_foo}(b)所示。

    \item \textbf{第9-10行}：foo()函数的第一条语句将`%ebp`推入堆栈以保存前一个帧指针。第二条
      使`%ebp`指向当前的堆栈帧。
      压入堆栈后的状态如图\ref{fig:enter_leave_foo}(c)所示。

    \item \textbf{第11行：`subl $8, %esp`}：
     `esp`指针被修改以分配空间（8字节）用于局部变量和传给`printf`的两个参数。由于foo()中没有局部变量，这8个字节全用于参数。
      如图\ref{fig:enter_leave_foo}(d)所示。

\end{itemize}

% -------------------------------------------
% SUBSECTION
% ------------------------------------------- 
\subsection{离开`foo()`函数}

现在控制权已经转到foo()函数中。让我们看看当函数返回时堆栈会发生什么变化。

\begin{itemize}
    \item \textbf{第16行：`leave`}：此指令隐含执行了两个指令（在早期的x86版本中是一个宏，但后来变成了指令）：
    ```assembly
        mov  %ebp, %esp
        pop  %ebp
    ```
     第一个指令释放为foo函数分配的空间；第二个指令恢复之前的帧指针。
      压入堆栈后的状态如图\ref{fig:enter_leave_foo}(e)所示。

    \item \textbf{第17行：`ret`}：此指令只是从堆栈中弹出返回地址，然后跳转到该返回地址。
     压入堆栈后的状态如图\ref{fig:enter_leave_foo}(f)所示。

    \item \textbf{第32行：`addl $4, %esp`}：进一步恢复堆栈，释放foo函数中分配的更多内存。
      如你所见，此时堆栈与在进入函数foo之前的状态（即，在第28行之前）完全相同。
\end{itemize}

% *******************************************
% SECTION
% *******************************************
\section{提交指南}
 
\input{\commonfolder/submission}

\end{document}
```